package cn.hejiang.mamis.access.controller;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;

import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
import com.nimbusds.oauth2.sdk.TokenRequest;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.oauth2.sdk.util.URLUtils;
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;

import cn.hejiang.mamis.access.properties.OidcProperties;
import cn.hejiang.mamis.core.entity.PlatSys;
import cn.hejiang.mamis.core.entity.PlatUser;
import cn.hejiang.mamis.core.entity.SessionDetails;
import cn.hejiang.mamis.core.entity.SysRole;
import cn.hejiang.mamis.core.service.PlatSysService;
import cn.hejiang.mamis.core.service.PlatUserService;
import cn.hejiang.mamis.core.service.ContextService;
import cn.hejiang.mamis.core.service.SysRoleService;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;

@Controller
@RequestMapping("/mamis/access/oidc")
@Slf4j
public class OidcController {
	static final String ID_PREFIX = "oidc:";
	static final String KEY_REDIR = "INIT_REDIR";
	static final String KEY_STATE = "OIDC_STATE";
	static final String KEY_NONCE = "OIDC_NONCE";
	static final String DO_LOGIN_URI = "mamis/access/oidc/doLogin";

	@Autowired
	OidcProperties props;

	@Autowired
	private PlatSysService platSysService;

	@Autowired
	private PlatUserService platUserService;

	@Autowired
	private SysRoleService sysRoleService;

	@Autowired
	private ContextService sessionService;

	@GetMapping("/login")
	public void login(HttpServletRequest request, HttpServletResponse response,
			@RequestParam(name = "redirect", required = false) String redirect) throws URISyntaxException, IOException {
		HttpSession session = request.getSession();

		// 保存重定向地址
		if (StrUtil.isNotBlank(redirect)) {
			session.setAttribute(KEY_REDIR, redirect);
		}

		// 生成实际登录地址
		String doLoginURL = getBaseURL(request) + DO_LOGIN_URI;

		// 生成 oidc 认证地址
		URI authURI = getAuthURI(session, doLoginURL);

		// 发起认证请求
		response.sendRedirect(authURI.toString());
	}

	@GetMapping("/doLogin")
	public void doLogin(HttpServletRequest request, HttpServletResponse response)
			throws IOException, ParseException, URISyntaxException {
		// 获取授权 code
		AuthorizationCode code = getAuthCode(request);

		// 通过 code 获得 token
		String doLoginURL = getBaseURL(request) + DO_LOGIN_URI;
		OIDCTokens tokens = getOIDCTokens(doLoginURL, code);

		// 通过 token 获得用户信息
		UserInfo userInfo = getUserInfo(tokens.getBearerAccessToken());

		// 查询本地账号
		final String userId = ID_PREFIX + userInfo.getClaim("sub", String.class);

		PlatUser user = platUserService.getUserById(userId);

		// 创建本地账号
		if (user == null) {
			final PlatSys sys = platSysService.getSysByCode(PlatSys.PLATFORM_CODE);

			user = new PlatUser();
			user.setId(userId);
			user.setLoginName(userInfo.getPreferredUsername());
			user.setName(userInfo.getFamilyName() + userInfo.getGivenName());
			user.setEmail(userInfo.getEmailAddress());
			user.setCreatedBy("oidc");
			user.setUpdatedBy("oidc");
			user.setOwnSysId(sys.getId());

			boolean ret = platUserService.save(user);
			if (ret && userInfo.getEmailVerified()) {
				if (StrUtil.isNotBlank(props.getInitialAdminIdentity())) {
					List<String> initialAdminEmails = Arrays.asList(props.getInitialAdminIdentity().split(","));

					// 关联到平台管理员角色，普通用户权限由管理员通过页面管理
					if (initialAdminEmails.contains(userInfo.getEmailAddress())) {
						// 关联到平台管理员角色
						SysRole adminRole = sysRoleService.getRoleByName("admin", sys.getId(), null);
						sysRoleService.addRoleUser(adminRole.getId(), userId, sys.getId(), "oidc");
					}
				}
			}
		}

		// 执行系统登录
		SessionDetails details = sessionService.newSession(request, user);

		// 重定向访问地址
		HttpSession session = request.getSession();
		String redirect = (String) session.getAttribute(KEY_REDIR);
		if (StrUtil.isBlank(redirect)) {
			redirect = URLEncoder.encode(getBaseURL(request), "utf-8");
		}
		response.sendRedirect(redirect);
	}

	@GetMapping("/logout")
	public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
		// 构造重定向地址
		StringBuilder builder = new StringBuilder(props.getEndSessionEndpoint());
		builder.append("?client_id=");
		builder.append(props.getClientId());
		builder.append("&post_logout_redirect_uri=");
		builder.append(URLEncoder.encode(getBaseURL(request), "utf-8"));

		response.sendRedirect(builder.toString());
	}

	private String getBaseURL(HttpServletRequest request) {
		String baseUrl = props.getExternalUrl();
		if (StrUtil.isBlank(baseUrl)) {
			baseUrl = request.getRequestURL().substring(0, request.getRequestURL().lastIndexOf("/mamis/access/oidc"));
		}

		if (!baseUrl.endsWith("/")) {
			baseUrl = baseUrl + "/";
		}

		return baseUrl;
	}

	private URI getAuthURI(HttpSession session, String redirect_uri) throws URISyntaxException {
		// The client ID provisioned by the OpenID provider when
		// the client was registered
		ClientID clientID = new ClientID(props.getClientId());

		// The client callback URL
		URI callback = new URI(redirect_uri);

		// Generate random state string to securely pair the callback to this request
		State state = new State();
		session.setAttribute(KEY_STATE, state);

		// Generate nonce for the ID token
		Nonce nonce = new Nonce();
		session.setAttribute(KEY_NONCE, nonce);

		// Compose the OpenID authentication request (for the code flow)
		AuthenticationRequest request = new AuthenticationRequest.Builder(new ResponseType("code"), new Scope("openid"),
				clientID, callback).endpointURI(new URI(props.getAuthEndpoint())).state(state).nonce(nonce).build();

		return request.toURI();
	}

	private AuthorizationCode getAuthCode(HttpServletRequest request) throws ParseException, URISyntaxException {
		AuthenticationResponse response = AuthenticationResponseParser
				.parse(new URI(request.getRequestURL().toString()), URLUtils.parseParameters(request.getQueryString()));

		// Check the state
		if (!response.getState().equals(request.getSession().getAttribute(KEY_STATE))) {
			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unexpected authentication response");
		}

		if (response instanceof AuthenticationErrorResponse) {
			// The OpenID provider returned an error
			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
					response.toErrorResponse().getErrorObject().toString());
		}

		// Retrieve the authorisation code, to use it later at the token endpoint
		AuthorizationCode code = response.toSuccessResponse().getAuthorizationCode();
		return code;
	}

	private OIDCTokens getOIDCTokens(String finishLoginURL, AuthorizationCode code)
			throws ParseException, IOException, URISyntaxException {
		// Construct the code grant from the code obtained from the authz endpoint
		// and the original callback URI used at the authz endpoint
		// AuthorizationCode code = new AuthorizationCode("xyz...");
		URI callback = new URI(finishLoginURL);
		AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback);

		// The credentials to authenticate the client at the token endpoint
		ClientID clientID = new ClientID(props.getClientId());
		Secret clientSecret = new Secret(props.getClientSecret());
		ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret);

		// The token endpoint
		URI tokenEndpoint = new URI(props.getTokenEndpoint());

		// Make the token request
		TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant);

		TokenResponse tokenResponse = OIDCTokenResponseParser.parse(request.toHTTPRequest().send());

		if (!tokenResponse.indicatesSuccess()) {
			// We got an error response...
			TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
					errorResponse.toHTTPResponse().getStatusMessage());
		}

		OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();

		// Get the ID and access token, the server may also return a refresh token
		OIDCTokens tokens = successResponse.getOIDCTokens();
		return tokens;
	}

	private UserInfo getUserInfo(BearerAccessToken token) throws IOException, ParseException, URISyntaxException {
		// The UserInfoEndpoint of the OpenID provider
		URI userInfoEndpoint = new URI(props.getUserinfoEndpoint());

		// Make the request
		HTTPResponse httpResponse = new UserInfoRequest(userInfoEndpoint, token).toHTTPRequest().send();

		// Parse the response
		UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);

		if (!userInfoResponse.indicatesSuccess()) {
			// The request failed, e.g. due to invalid or expired token
			System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getCode());
			System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getDescription());
			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
					userInfoResponse.toErrorResponse().getErrorObject().getDescription());
		}

		// Extract the claims
		UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo();
		return userInfo;
	}
}